'use client';
import * as React from 'react';
import { RefObject } from '@mui/x-internals/types';
import ownerDocument from '@mui/utils/ownerDocument';
import { loadStyleSheets } from '@mui/x-internals/export';
import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity';
import { GridPrintExportApi } from '../../../models/api/gridPrintExportApi';
import { useGridLogger } from '../../utils/useGridLogger';
import { gridExpandedRowCountSelector } from '../filter/gridFilterSelector';
import { DataGridProcessedProps } from '../../../models/props/DataGridProps';
import { GridPrintExportOptions } from '../../../models/gridExport';
import { GridValidRowModel } from '../../../models/gridRows';
import { GridInitialStateCommunity, GridStateCommunity } from '../../../models/gridStateCommunity';
import {
  gridColumnDefinitionsSelector,
  gridColumnVisibilityModelSelector,
} from '../columns/gridColumnsSelector';
import { gridClasses } from '../../../constants/gridClasses';
import { useGridApiMethod } from '../../utils/useGridApiMethod';
import { GRID_ID_AUTOGENERATED } from '../rows/gridRowsUtils';
import { defaultGetRowsToExport, getColumnsToExport } from './utils';
import { getDerivedPaginationModel } from '../pagination/useGridPaginationModel';
import { GridPipeProcessor, useGridRegisterPipeProcessor } from '../../core/pipeProcessing';
import { GridExportDisplayOptions, GridPrintExportMenuItem } from '../../../components/toolbar';
import { GRID_CHECKBOX_SELECTION_COL_DEF } from '../../../colDef/gridCheckboxSelectionColDef';
import { scrollbarSizeCssExpression } from '../../../components/virtualization/GridVirtualScrollbar';

const DEBUG_MODE = false;

function raf() {
  return new Promise<void>((resolve) => {
    requestAnimationFrame(() => {
      resolve();
    });
  });
}

type PrintWindowOnLoad = (
  printWindow: HTMLIFrameElement,
  options?: Pick<
    GridPrintExportOptions,
    | 'copyStyles'
    | 'bodyClassName'
    | 'pageStyle'
    | 'hideToolbar'
    | 'hideFooter'
    | 'includeCheckboxes'
    | 'getRowsToExport'
  >,
) => void;

function buildPrintWindow(title?: string): HTMLIFrameElement {
  const iframeEl = document.createElement('iframe');
  iframeEl.style.position = 'absolute';
  iframeEl.style.width = '0px';
  iframeEl.style.height = '0px';
  if (DEBUG_MODE) {
    iframeEl.style.width = '100%';
    iframeEl.style.height = '10000px';
  }
  iframeEl.title = title || document.title;
  return iframeEl;
}

/**
 * @requires useGridColumns (state)
 * @requires useGridFilter (state)
 * @requires useGridSorting (state)
 * @requires useGridParamsApi (method)
 */
export const useGridPrintExport = (
  apiRef: RefObject<GridPrivateApiCommunity>,
  props: Pick<DataGridProcessedProps, 'pagination' | 'columnHeaderHeight' | 'headerFilterHeight'>,
): void => {
  const hasRootReference = apiRef.current.rootElementRef.current !== null;
  const logger = useGridLogger(apiRef, 'useGridPrintExport');
  const doc = React.useRef<Document | null>(null);
  const previousGridState = React.useRef<GridInitialStateCommunity | null>(null);
  const previousColumnVisibility = React.useRef<{ [key: string]: boolean }>({});
  const previousRows = React.useRef<GridValidRowModel[]>([]);
  const previousVirtualizationState = React.useRef<GridStateCommunity['virtualization']>(null);

  React.useEffect(() => {
    doc.current = ownerDocument(apiRef.current.rootElementRef!.current!);
  }, [apiRef, hasRootReference]);

  // Returns a promise because updateColumns triggers state update and
  // the new state needs to be in place before the grid can be sized correctly
  const updateGridColumnsForPrint = React.useCallback(
    (fields?: string[], allColumns?: boolean, includeCheckboxes?: boolean) =>
      new Promise<void>((resolve) => {
        const exportedColumnFields = getColumnsToExport({
          apiRef,
          options: { fields, allColumns },
        }).map((column) => column.field);

        const columns = gridColumnDefinitionsSelector(apiRef);

        const newColumnVisibilityModel: Record<string, boolean> = {};
        columns.forEach((column) => {
          newColumnVisibilityModel[column.field] = exportedColumnFields.includes(column.field);
        });

        if (includeCheckboxes) {
          newColumnVisibilityModel[GRID_CHECKBOX_SELECTION_COL_DEF.field] = true;
        }

        apiRef.current.setColumnVisibilityModel(newColumnVisibilityModel);
        resolve();
      }),
    [apiRef],
  );

  const updateGridRowsForPrint = React.useCallback(
    (getRowsToExport: NonNullable<GridPrintExportOptions['getRowsToExport']>) => {
      const rowsToExportIds = getRowsToExport({ apiRef });

      const newRows = rowsToExportIds.reduce<GridValidRowModel[]>((acc, id) => {
        const row = apiRef.current.getRow(id);
        if (!row[GRID_ID_AUTOGENERATED]) {
          acc.push(row);
        }
        return acc;
      }, [] as GridValidRowModel[]);

      apiRef.current.setRows(newRows);
    },
    [apiRef],
  );

  const handlePrintWindowLoad: PrintWindowOnLoad = React.useCallback(
    (printWindow, options): void => {
      const normalizeOptions = {
        copyStyles: true,
        hideToolbar: false,
        hideFooter: false,
        includeCheckboxes: false,
        ...options,
      };

      const printDoc = printWindow.contentDocument;

      if (!printDoc) {
        return;
      }

      const gridRootElement = apiRef.current.rootElementRef.current;
      const gridClone = gridRootElement!.cloneNode(true) as HTMLElement;
      const virtualScrollerContent = gridClone.querySelector<HTMLElement>(
        `.${gridClasses.virtualScrollerContent}`,
      )!;

      // Allow to overflow to not hide the border of the last row
      const gridMain: HTMLElement | null = gridClone.querySelector(`.${gridClasses.main}`);
      gridMain!.style.overflow = 'visible';

      gridClone.querySelector<HTMLElement>(
        `.${gridClasses.virtualScrollerRenderZone}`,
      )!.style.position = 'static';

      virtualScrollerContent.style.flexBasis = 'auto';

      gridClone.querySelector<HTMLElement>(`.${gridClasses['scrollbar--vertical']}`)?.remove();
      if (
        !(
          virtualScrollerContent.nextSibling instanceof HTMLElement &&
          virtualScrollerContent.nextSibling.classList.contains(gridClasses.filler)
        )
      ) {
        const filler = document.createElement('div');
        filler.style.height = scrollbarSizeCssExpression;
        virtualScrollerContent.insertAdjacentElement('afterend', filler);
      }

      const gridFooterElement = gridClone.querySelector<HTMLElement>(
        `.${gridClasses.footerContainer}`,
      );

      if (normalizeOptions.hideToolbar) {
        gridClone.querySelector(`.${gridClasses.toolbar}`)?.remove();
      }

      if (normalizeOptions.hideFooter && gridFooterElement) {
        gridFooterElement.remove();
      }

      gridClone.style.height = 'auto';
      // The height above does not include grid border width, so we need to exclude it
      gridClone.style.boxSizing = 'content-box';

      if (!normalizeOptions.hideFooter && gridFooterElement) {
        // the footer is always being placed at the bottom of the page as if all rows are exported
        // so if getRowsToExport is being used to only export a subset of rows then we need to
        // adjust the footer position to be correctly placed at the bottom of the grid
        gridFooterElement.style.width = '100%';
      }

      // printDoc.body.appendChild(gridClone); should be enough but a clone isolation bug in Safari
      // prevents us to do it
      const container = document.createElement('div');
      container.appendChild(gridClone);
      // To avoid an empty page in start on Chromium based browsers
      printDoc.body.style.marginTop = '0px';
      printDoc.body.innerHTML = container.innerHTML;

      const defaultPageStyle =
        typeof normalizeOptions.pageStyle === 'function'
          ? normalizeOptions.pageStyle()
          : normalizeOptions.pageStyle;
      if (typeof defaultPageStyle === 'string') {
        // TODO custom styles should always win
        const styleElement = printDoc.createElement('style');
        styleElement.appendChild(printDoc.createTextNode(defaultPageStyle));
        printDoc.head.appendChild(styleElement);
      }

      if (normalizeOptions.bodyClassName) {
        printDoc.body.classList.add(...normalizeOptions.bodyClassName.split(' '));
      }

      let stylesheetLoadPromises: Promise<void>[] = [];

      if (normalizeOptions.copyStyles) {
        const rootCandidate = gridRootElement!.getRootNode();
        const root =
          rootCandidate.constructor.name === 'ShadowRoot'
            ? (rootCandidate as ShadowRoot)
            : doc.current;

        stylesheetLoadPromises = loadStyleSheets(printDoc, root!);
      }

      // Trigger print
      if (process.env.NODE_ENV !== 'test' && !DEBUG_MODE) {
        // wait for remote stylesheets to load
        Promise.all(stylesheetLoadPromises).then(() => {
          printWindow.contentWindow!.print();
        });
      }
    },
    [apiRef, doc],
  );

  const handlePrintWindowAfterPrint = React.useCallback(
    (printWindow: HTMLIFrameElement): void => {
      // Remove the print iframe
      doc.current!.body.removeChild(printWindow);

      // Revert grid to previous state
      apiRef.current.restoreState(previousGridState.current || {});
      if (!previousGridState.current?.columns?.columnVisibilityModel) {
        // if the apiRef.current.exportState(); did not exported the column visibility, we update it
        apiRef.current.setColumnVisibilityModel(previousColumnVisibility.current);
      }

      apiRef.current.setState((state) => ({
        ...state,
        virtualization: previousVirtualizationState.current!,
      }));
      apiRef.current.setRows(previousRows.current);

      // Clear local state
      previousGridState.current = null;
      previousColumnVisibility.current = {};
      previousRows.current = [];
    },
    [apiRef],
  );

  const exportDataAsPrint = React.useCallback<GridPrintExportApi['exportDataAsPrint']>(
    async (options) => {
      logger.debug(`Export data as Print`);

      if (!apiRef.current.rootElementRef!.current) {
        throw new Error('MUI X: No grid root element available.');
      }

      previousGridState.current = apiRef.current.exportState();
      // It appends that the visibility model is not exported, especially if columnVisibility is not controlled
      previousColumnVisibility.current = gridColumnVisibilityModelSelector(apiRef);
      previousRows.current = apiRef.current
        .getSortedRows()
        .filter((row) => !row[GRID_ID_AUTOGENERATED]);

      if (props.pagination) {
        const visibleRowCount = gridExpandedRowCountSelector(apiRef);
        const paginationModel = {
          page: 0,
          pageSize: visibleRowCount,
        };
        apiRef.current.setState((state) => ({
          ...state,
          pagination: {
            ...state.pagination,
            paginationModel: getDerivedPaginationModel(
              state.pagination,
              // Using signature `DataGridPro` to allow more than 100 rows in the print export
              'DataGridPro',
              paginationModel,
            ),
          },
        }));
      }
      previousVirtualizationState.current = apiRef.current.state.virtualization;
      apiRef.current.unstable_setVirtualization(false);

      await updateGridColumnsForPrint(
        options?.fields,
        options?.allColumns,
        options?.includeCheckboxes,
      );

      updateGridRowsForPrint(options?.getRowsToExport ?? defaultGetRowsToExport);

      await raf(); // wait for the state changes to take action
      const printWindow = buildPrintWindow(options?.fileName);
      if (process.env.NODE_ENV === 'test') {
        doc.current!.body.appendChild(printWindow);
        // In test env, run the all pipeline without waiting for loading
        handlePrintWindowLoad(printWindow, options);
        handlePrintWindowAfterPrint(printWindow);
      } else {
        printWindow.onload = () => {
          handlePrintWindowLoad(printWindow, options);

          const mediaQueryList = printWindow.contentWindow!.matchMedia('print');
          mediaQueryList.addEventListener('change', (mql) => {
            const isAfterPrint = mql.matches === false;
            if (isAfterPrint) {
              handlePrintWindowAfterPrint(printWindow);
            }
          });
        };
        doc.current!.body.appendChild(printWindow);
      }
    },
    [
      props,
      logger,
      apiRef,
      handlePrintWindowLoad,
      handlePrintWindowAfterPrint,
      updateGridColumnsForPrint,
      updateGridRowsForPrint,
    ],
  );

  const printExportApi: GridPrintExportApi = {
    exportDataAsPrint,
  };

  useGridApiMethod(apiRef, printExportApi, 'public');

  /**
   * PRE-PROCESSING
   */
  const addExportMenuButtons = React.useCallback<GridPipeProcessor<'exportMenu'>>(
    (
      initialValue,
      options: { printOptions: GridPrintExportOptions & GridExportDisplayOptions },
    ) => {
      if (options.printOptions?.disableToolbarButton) {
        return initialValue;
      }
      return [
        ...initialValue,
        {
          component: <GridPrintExportMenuItem options={options.printOptions} />,
          componentName: 'printExport',
        },
      ];
    },
    [],
  );

  useGridRegisterPipeProcessor(apiRef, 'exportMenu', addExportMenuButtons);
};
